﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Netfraction.Common;

namespace Netfraction.ShareManager
{
    public class ShareManager
    {
        Database.HashDatabaseDataContext hashDataContext;
        Dictionary<string, Network.Protocol.ProtocolFeature> mFileListNameFeatures;
        VFSDirectoryNode mRootNode;
        Hashing.HashPool mHashPool;
        private Dictionary<VFSFileNode, string> mHashPending;

        public ShareManager()
        {
            hashDataContext = new Database.HashDatabaseDataContext(Netfraction.Properties.Settings.Default.HashDBConnString);

            mFileListNameFeatures = new Dictionary<string, Network.Protocol.ProtocolFeature>();

            // we should still be able to populate the VirtualFileSystem without loading the hashes just yet
            // the hashes will be loaded after SUP negotiation of the session hash algorithm
            mHashPool = null;

            mHashPending = new Dictionary<VFSFileNode, string>();

            mRootNode = new VFSDirectoryNode(null, "/", "", DateTime.MinValue);
        }

        #region File List members
        public MemoryStream CreateFileListStream(string fileListName)
        {
            MemoryStream mStream = new MemoryStream();
            
            var featList = from p in mFileListNameFeatures
                       where p.Key == fileListName
                       select p.Value;
            if (featList.Count() > 0)
            {
                Network.Protocol.IProtocolFileListFeature feat = featList.First() as Network.Protocol.IProtocolFileListFeature;
                mStream = feat.WriteToFileList(mStream);
            }
            else
            {
                return null;
            }
            return new MemoryStream(mStream.ToArray());
        }

        public MemoryStream ReadFileListFromStream(string fileListName, MemoryStream stream)
        {
            MemoryStream mStream = new MemoryStream(stream.ToArray());
            var featList = from p in mFileListNameFeatures
                           where p.Key == fileListName
                           select p.Value;
            if (featList.Count() > 0)
            {
                Network.Protocol.IProtocolFileListFeature feat = featList.First() as Network.Protocol.IProtocolFileListFeature;
                mStream = feat.ReadFromFileList(mStream);
            }
            else
            {
                return null;
            }
            return mStream;
        }

        public bool RegisterFileListName(string name, Network.Protocol.ProtocolFeature feature)
        {
            if (mFileListNameFeatures.ContainsKey(name))
            {
                return false;
            }

            mFileListNameFeatures.Add(name, feature);
            return true;
        }

        public List<string> FileListNames
        {
            get { return mFileListNameFeatures.Keys.ToList(); }
        }
        #endregion File List members

        public void AddDirectory(DirectoryInfo dir, string sharedFolderName) // add Name parameter
        {
            if (mHashPool != null)
            {
                mHashPool.FileHashed += new EventHandler<Hashing.HashEventArgs>(OnFileHashed);
            }
            
            // Check if this path is already included in the tree
            var exists = from p in hashDataContext.VirtualTreeMap
                         where dir.FullName.StartsWith(p.PhysicalPath)
                         select p;

            if (exists.Count() > 0)
            {
                // it exists already
                return;
            }

            ///////////////////////////////////////////////////////
            
            Database.VirtualFileTree rootNode = (from p in hashDataContext.VirtualFileTree
                                                 where p.NodeID == 0
                                                 select p).First();

            VFSDirectoryNode newDirectory = new VFSDirectoryNode(mRootNode, sharedFolderName, dir.Name, dir.LastWriteTimeUtc);
            TryLoadDirectory(newDirectory, dir.FullName, mHashPool.ActionQueue.AbortEvent, rootNode);
            mRootNode.Subdirectories.Add(newDirectory);
        }

        private void TryLoadDirectory(VFSDirectoryNode directory, string path, System.Threading.ManualResetEvent abortEvent, Database.VirtualFileTree vftParentNode)
        {
            DirectoryInfo dirInfo = TryGetDirectoryInfo(path);
            if (dirInfo != null && dirInfo.Exists)
            {
                try
                {
                    LoadDirectory(directory, dirInfo, abortEvent, vftParentNode);
                }
                catch (Exception)
                {
                    //log.Error("Loading directory failed", e);
                    System.Diagnostics.Debug.WriteLine(String.Format("Loading directory failed #1: {0}", path));
                }
            }
            else
            {
                directory.Subdirectories = new List<VFSDirectoryNode>();
                directory.Files = new List<VFSFileNode>();
            }
        }

        private void LoadDirectory(VFSDirectoryNode directory, DirectoryInfo dirInfo, System.Threading.ManualResetEvent abortEvent, Database.VirtualFileTree vftParentNode)
        {
            if (vftParentNode.NodeID == 0)
            {
                Database.VirtualTreeMap vMap = new Netfraction.Database.VirtualTreeMap();
                vMap.PhysicalPath = dirInfo.FullName;
                vMap.VirtualName = directory.Name;    // change this later to be user selected name
                vMap.NodeID = vftParentNode.NodeID;

                hashDataContext.VirtualTreeMap.InsertOnSubmit(vMap);

                hashDataContext.SubmitChanges();
            }

            Database.VirtualFileTree vftCurrentNode = AddDirectoryToDB(dirInfo, vftParentNode);

            // Load the directories
            foreach (DirectoryInfo childDirInfo in dirInfo.GetDirectories())
            {
                if (abortEvent.WaitOne(0, false))
                {
                    return;
                }
                bool dirIsHiddenOrSystem = (childDirInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || (childDirInfo.Attributes & FileAttributes.System) == FileAttributes.System
                    || (childDirInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint || (childDirInfo.Attributes & FileAttributes.Temporary) == FileAttributes.Temporary;

                if (!dirIsHiddenOrSystem)
                {
                    VFSDirectoryNode childDir = new VFSDirectoryNode(directory, childDirInfo.Name, childDirInfo.Name, childDirInfo.LastWriteTimeUtc);
                    directory.Subdirectories.Add(childDir);

                    try
                    {
                        LoadDirectory(childDir, childDirInfo, abortEvent, vftCurrentNode);
                    }
                    catch (Exception)
                    {
                        //log.Error("Loading directory failed", e);
                        System.Diagnostics.Debug.WriteLine(String.Format("Loading directory failed #2: {0}", dirInfo.FullName));
                    }
                }
            }

            // Load the files
            foreach (FileInfo fileInfo in dirInfo.GetFiles())
            {
                if (abortEvent.WaitOne(0, false))
                {
                    return;
                }
                bool fileIsHidden = (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || (fileInfo.Attributes & FileAttributes.System) == FileAttributes.System
                    || (fileInfo.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint || (fileInfo.Attributes & FileAttributes.Temporary) == FileAttributes.Temporary;

                if (!fileIsHidden)
                {
                    VFSFileNode file = new VFSFileNode(directory, fileInfo.Name, fileInfo.Length, fileInfo.LastWriteTimeUtc, "");
                    directory.Files.Add(file);

                    lock (mHashPool)
                    {
                        if (true)//!mHashPool.HasHash(fileInfo))
                        {
                            Hash(file, fileInfo);
                        }
                        /*else if (file.RootHash == null)
                        {
                            file.RootHash = mHashPool.GetRootHash(fileInfo);
                        }*/
                    }
                }
            }
        }

        private static DirectoryInfo TryGetDirectoryInfo(string path)
        {
            try
            {
                return new DirectoryInfo(path);
            }
            catch
            {
                return null;
            }
        }

        private void Hash(VFSFileNode file, FileInfo fileInfo)
        {
            lock (mHashPool)
            {
                mHashPending.Add(file, fileInfo.FullName);
                mHashPool.HashFile (fileInfo);
            }
        }

        /*public void AddDirectory(DirectoryInfo dir) // add Name parameter
        {
            Database.VirtualFileTree rootNode = (from p in hashDataContext.VirtualFileTree
                                                 where p.NodeID == 0
                                                 select p).First();
            Database.VirtualFileTree vft = AddDirectoryToDB(dir, rootNode);

            Database.VirtualTreeMap vMap = new Netfraction.Database.VirtualTreeMap();
            vMap.PhysicalPath = dir.FullName;
            vMap.VirtualName = dir.Name;    // change this later to be user selected name
            vMap.NodeID = vft.NodeID;

            hashDataContext.VirtualTreeMap.InsertOnSubmit(vMap);
            hashDataContext.SubmitChanges();

            // now need to add each sub folder in the same manner, and queue all files to be hashed.
            // make sure each folder is submitted before marking its files for hashing
            List<string> pendingFiles = new List<string>();
            //AddFilesToPendingList(dir, ref pendingFiles);

            RecursiveAddDirectory(dir, ref pendingFiles, vft);

            // so we have all our files to be hashed in pending files... hash them
            foreach (string file in pendingFiles)
            {
                //Hash(file, new FileInfo(file));
            }
        }*/

        private Database.VirtualFileTree AddDirectoryToDB(DirectoryInfo dir, Database.VirtualFileTree parent)
        {
            Database.VirtualFileTree vft = new Database.VirtualFileTree();
            /*vft.Name = dir.Name;
            vft.DateModified = dir.LastWriteTimeUtc;
            vft.ParentID = parent.NodeID;
            vft.Depth = (byte) (parent.Depth + 1);

            hashDataContext.VirtualFileTree.InsertOnSubmit(vft);
            hashDataContext.SubmitChanges();*/

            vft = (from p in hashDataContext.VirtualFileTree
                   where p.ParentID == parent.NodeID
                   && p.Name == dir.Name
                   select p).First();
            return vft;
        }

        /*private void RecursiveAddDirectory(DirectoryInfo dir, ref List<string> pendingFiles, Database.VirtualFileTree vft)
        {
            Database.VirtualFileTree subNode;
            foreach (DirectoryInfo subDir in dir.GetDirectories())
            {
                subNode = AddDirectoryToDB(subDir, vft);
                RecursiveAddDirectory(subDir, ref pendingFiles, subNode);
            }
            //AddFilesToPendingList(dir, ref pendingFiles);
        }*/

        /*private static void AddFilesToPendingList(DirectoryInfo dir, ref List<string> pendingFiles)
        {
            //List<string> pendingFiles = new List<string>();
            foreach (FileInfo f in dir.GetFiles())
            {
                // don't index files that are Hidden, System, ReparsePoints or Temporary
                if (!((f.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden || (f.Attributes & FileAttributes.System) == FileAttributes.System
                    || (f.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint || (f.Attributes & FileAttributes.Temporary) == FileAttributes.Temporary))
                {
                    pendingFiles.Add(f.FullName);
                }
            }
        }*/

        public void OnFileHashed(object obj, Hashing.HashEventArgs args)
        {
            System.Diagnostics.Debug.WriteLine(String.Format("Start OnFileHashed: {0}", DateTime.Now));
            Database.VirtualFileTree vft = new Database.VirtualFileTree();
            vft.Name = args.FileInfo.Name;
            vft.DateModified = args.FileInfo.LastWriteTimeUtc;
            Database.VirtualFileTree parentNode = MapFileToTreeNode(args.FileInfo);
            if (parentNode == null)
            {
                return;
            }

            System.Diagnostics.Debug.WriteLine(String.Format("OnFileHashed Parent Node Found: {0}", DateTime.Now));

            vft.ParentID = parentNode.NodeID;
            if (parentNode.Depth.HasValue)
            {
                vft.Depth = (byte) (parentNode.Depth.Value + 1);
            }
            
            hashDataContext.VirtualFileTree.InsertOnSubmit(vft);
            hashDataContext.SubmitChanges();

            System.Diagnostics.Debug.WriteLine(String.Format("OnFileHashed VFT Entry Added: {0}", DateTime.Now));

            var curNode = from p in hashDataContext.VirtualFileTree
                                                where p.ParentID == vft.ParentID
                                                && p.Name == vft.Name
                                                select p;

            if (curNode.Count() == 0)
            {
                return;
            }
            System.Diagnostics.Debug.WriteLine(String.Format("OnFileHashed curNodeFound: {0}", DateTime.Now));

            Database.HashRoots hr = new Database.HashRoots();
            hr.HashRoot = Base32.Encode(args.HashTree.Root);
            hr.BlockSize = args.HashTree.BlockSize;
            hr.NodeID = curNode.First().NodeID;

            if (args.HashTree.LeafCount > 1)
            {
                var startIndex = (from p in hashDataContext.HashTable
                                  select (int?)p.HashId).Max();
                hr.StartIndex = (startIndex.HasValue ? startIndex.Value : 0) + 1;

                System.Diagnostics.Debug.WriteLine(String.Format("OnFileHashed Begin Leaves: {0}", DateTime.Now));
                Database.HashTable ht;
                foreach (byte[] leaf in args.HashTree.Leaves)
                {
                    ht = new Database.HashTable();
                    ht.HashValue = leaf;

                    hashDataContext.HashTable.InsertOnSubmit(ht);
                }
                System.Diagnostics.Debug.WriteLine(String.Format("OnFileHashed End Leaves: {0}", DateTime.Now));
            }
            else
            {
                hr.StartIndex = null;
            }

            hashDataContext.HashRoots.InsertOnSubmit(hr);

            hashDataContext.SubmitChanges();
            System.Diagnostics.Debug.WriteLine(String.Format("End OnFileHashed: {0}", DateTime.Now));
        }

        private Database.VirtualFileTree MapFileToTreeNode(FileInfo fileInfo)
        {
            var virtualRoots = from p in hashDataContext.VirtualTreeMap
                               select p;

            foreach (Database.VirtualTreeMap listItem in virtualRoots)
            {
                if (fileInfo.FullName.StartsWith(listItem.PhysicalPath, StringComparison.InvariantCultureIgnoreCase))
                {
                    int rootId = listItem.NodeID;
                    string rootPath = listItem.PhysicalPath;
                    string virtualPath = fileInfo.FullName.Remove(0, rootPath.Length);

                    Database.VirtualFileTree curNode = hashDataContext.VirtualFileTree.Where(p => p.NodeID == rootId).First();
                    foreach (string subPath in virtualPath.Split(Path.DirectorySeparatorChar))
                    {
                        var childNodes = hashDataContext.VirtualFileTree.Where(p => p.ParentID == curNode.NodeID);
                        foreach (Database.VirtualFileTree subNode in childNodes)
                        {
                            if (subNode.Name == subPath)
                            {
                                curNode = subNode;
                                break;
                            }
                        }
                    }
                    if (curNode.Name == fileInfo.Directory.Name)
                    {
                        return curNode;
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            return null;
        }

        private Database.VirtualFileTree MapDirectoryToTreeNode(DirectoryInfo dirInfo)
        {
            var virtualRoots = from p in hashDataContext.VirtualTreeMap
                               select p;

            foreach (Database.VirtualTreeMap listItem in virtualRoots)
            {
                if (dirInfo.FullName.StartsWith(listItem.PhysicalPath, StringComparison.InvariantCultureIgnoreCase))
                {
                    int rootId = listItem.NodeID;
                    string rootPath = listItem.PhysicalPath;
                    string virtualPath = dirInfo.FullName.Remove(0, rootPath.Length);

                    Database.VirtualFileTree curNode = hashDataContext.VirtualFileTree.Where(p => p.NodeID == rootId).First();
                    foreach (string subPath in virtualPath.Split(new char[]{Path.DirectorySeparatorChar}, StringSplitOptions.RemoveEmptyEntries))
                    {
                        var childNodes = hashDataContext.VirtualFileTree.Where(p => p.ParentID == curNode.NodeID);
                        foreach (Database.VirtualFileTree subNode in childNodes)
                        {
                            if (subNode.Name == subPath)
                            {
                                curNode = subNode;
                                break;
                            }
                        }
                    }
                    if (curNode.Name == dirInfo.Name)
                    {
                        return curNode;
                    }
                    else
                    {
                        return null;
                    }
                }
            }
            return null;
        }

        #region Singleton pattern
        private static ShareManager mInstance;

        public static ShareManager GetInstance()
        {
            // Check instance
            if (ShareManager.mInstance == null)
            {
                // Initialize instance
                ShareManager.mInstance = new ShareManager();
            }

            // Return instance
            return ShareManager.mInstance;
        }

        #endregion Singleton pattern

        internal void SetHashAlgorithm(System.Security.Cryptography.HashAlgorithm hashAlg)
        {
            // changing the algorithm when one is already loaded would mean making it reload all the hashes for the other algorithm
            if (mHashPool == null)
            {
                mHashPool = new Hashing.HashPool(hashAlg);
                mRootNode = ShareLoader.LoadVirtualTree(hashDataContext);
            }
        }
    }
}
